iT邦幫忙

2024 iThome 鐵人賽

DAY 29
1
Modern Web

Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器系列 第 29

Day 29: Nuxt3 中的路由管理以及 Middleware:如何結合 TypeScript 實現靈活的路由系統

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20240927/20117461VF1SaaSDew.jpg

簡介

Nuxt3 提供了強大而靈活的路由系統,結合 TypeScript 的靜態類型檢查,可以大大提高開發效率和代碼質量。本文將深入探討 Nuxt3 中的路由管理和中間件(Middleware)的使用,並結合先前學習的概念,如 Pinia、Zod、Vee-Validate 等,構建一個健壯的路由系統。我們還將討論 Nuxt3 的生命週期、cookies 的應用,以及如何處理客戶端存儲和 hydration 問題。

Nuxt3 路由系統概述

Nuxt3 使用基於文件系統的路由,這意味著你在 pages 目錄中創建的 Vue 組件會自動映射到相應的路由。

步驟 1: 基本路由設置

pages 目錄下創建以下文件結構:

pages/
  index.vue
  about.vue
  users/
    [id].vue

或是使用指令創建

bunx nuxi add page index
bunx nuxi add page about
bunx nuxi add page users/[id]

這將自動生成以下路由:

  • /: 對應 index.vue
  • /about: 對應 about.vue
  • /users/:id: 對應 users/[id].vue

實現 Middleware

Nuxt3 支持兩種類型的中間件:全局中間件和路由中間件。

步驟 2: 創建 Server Middleware

server/middleware 目錄下創建 auth.ts

import { defineEventHandler, parseCookies } from 'h3'
import * as zod from 'zod'

const userSchema = zod.object({
  id: zod.number(),
  role: zod.enum(['user', 'admin'])
})

export default defineEventHandler((event) => {
  const cookies = parseCookies(event)
  const userCookie = cookies.user

  if (!userCookie) {
    return
  }

  try {
    const user = userSchema.parse(JSON.parse(userCookie))
    event.context.user = user
  } catch (error) {
    console.error('Invalid user data in cookie')
  }
})

步驟 3: 創建 Client Middleware

middleware 目錄下創建 auth.ts

import { useUserStore } from '~/stores/userStore'
import { storeToRefs } from 'pinia'

export default defineNuxtRouteMiddleware((to, from) => {
  const userStore = useUserStore()
  const { user } = storeToRefs(userStore)

  if (!user.value && to.path !== '/login') {
    return navigateTo('/login')
  }
})

步驟 4: 使用 Middleware

pages 目錄下的 Vue 組件中使用中間件:

<script setup lang="ts">
definePageMeta({
  middleware: ['auth']
})
</script>

<template>
  <div>
    <h1>Protected Page</h1>
  </div>
</template>

整合 Pinia 和 Zod

步驟 5: 創建和使用 Pinia Store

stores 目錄下創建 userStore.ts

import { defineStore } from 'pinia'
import * as zod from 'zod'

const userSchema = z.object({
  id: zod.number(),
  name: zod.string(),
  email: zod.string().email(),
  role: zod.enum(['user', 'admin'])
})

type User = zod.infer<typeof userSchema>

export const useUserStore = defineStore('user', () => {
  const user = ref<UserSchema | null>(null);

  // methods::
  const fetchUser = async (): Promise<void> => {
    const response = await $fetch('/api/user', {
      method: 'GET',
    });
    const validator = userSchema.safeParse(response);
    if (!validator.success) {
      throw new TypeError('validator error');
    }
    user.value = validator.data;
  };

  const logout = (): void => {
    user.value = null;
  };

  return {
    // state::
    user,
    // methods::
    fetchUser,
    logout,
  }
});

if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot));
}

使用 Composables 和 @vueuse/core

步驟 6: 創建自定義 Composable

composables 目錄下創建 useAuth.ts

import { useUserStore } from '~/stores/userStore'
import { useLocalStorage } from '@vueuse/core'

export function useAuth() {
  const userStore = useUserStore()

  const isAuthenticated = computed(() => !!userStore.user)

  const login = async (email: string, password: string) => {
    // 在實際應用中,這裡應該調用登錄 API
    await userStore.fetchUser()
    if (process.client) {
      useLocalStorage('auth_token', 'fake_token')
    }
  }

  const logout = () => {
    userStore.logout()
    if (process.client) {
      useLocalStorage('auth_token', null)
    }
  }

  return {
    isAuthenticated,
    login,
    logout,
  }
}

處理 Cookies 和 Client-only 存儲

步驟 7: 使用 useCookie 和 useState

pages/profile.vue 中:

<template>
  <div>
    <h1>Welcome, {{ user?.name }}</h1>
    <p>Theme: {{ theme }}</p>
    <button @click="toggleTheme">Toggle Theme</button>
  </div>
</template>

<script setup lang="ts">
import { useUserStore } from '~/stores/userStore'

const userStore = useUserStore()
const { user } = storeToRefs(userStore)

const theme = useState('theme', () => 'light')
const toggleTheme = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

const themeCookie = useCookie('theme')
watch(theme, (newTheme) => {
  themeCookie.value = newTheme
})

onMounted(() => {
  if (themeCookie.value) {
    theme.value = themeCookie.value
  }
})
</script>

解決 Hydration 問題

步驟 8: 使用 ClientOnly 組件

在需要客戶端渲染的組件中使用 <ClientOnly> 包裹:

<template>
  <div>
    <h1>User Profile</h1>
    <ClientOnly>
      <pre>{{ userJson }}</pre>
      <template #fallback>
        <p>Loading user data...</p>
      </template>
    </ClientOnly>
  </div>
</template>

<script setup lang="ts">
import { useUserStore } from '~/stores/userStore'

const userStore = useUserStore()
const { user } = storeToRefs(userStore)

const userJson = computed(() => JSON.stringify(user.value, null, 2))
</script>

Nuxt3 生命週期

Nuxt3 提供了幾個關鍵的生命週期鉤子:

  • useHead: 用於管理頁面的 head 信息
  • useFetch: 用於在組件加載前獲取數據
  • onMounted: 在組件掛載後執行
  • onUnmounted: 在組件卸載前執行

步驟 9: 使用生命週期鉤子

pages/users/[id].vue 中:

<script setup lang="ts">
const route = useRoute()
const userId = computed(() => route.params.id as string)

useHead({
  title: computed(() => `User ${userId.value}`),
})

const { data: user, pending, error } = await useFetch(`/api/users/${userId.value}`)

onMounted(() => {
  console.log('Component mounted')
})

onUnmounted(() => {
  console.log('Component will unmount')
})
</script>

<template>
  <div>
    <h1>User Details</h1>
    <p v-if="pending">Loading...</p>
    <p v-else-if="error">Error: {{ error.message }}</p>
    <div v-else>
      <p>Name: {{ user.name }}</p>
      <p>Email: {{ user.email }}</p>
    </div>
  </div>
</template>

結論

在本文中,我們深入探討了 Nuxt3 中的路由管理和中間件的使用,並結合了 TypeScript、Pinia、等技術來構建一個強大而靈活的路由系統。我們還討論了如何處理 cookies、客戶端存儲和 hydration 問題,以及如何利用 Nuxt3 的生命週期鉤子來優化應用性能。

通過實施這些最佳實踐,開發者可以充分利用 Nuxt3 的強大功能,構建出類型安全、高效且易於維護的現代 Web 應用。同時,整合諸如 Pinia ,進一步增強了應用的可靠性和開發效率。

在處理客戶端特定的操作時,始終要考慮服務器端渲染(SSR)的環境,並適當使用 <ClientOnly> 組件和條件性的代碼執行來避免潛在的 hydration 問題。此外,利用 Nuxt3 提供的 useFetch$fetch 方法進行數據獲取,可以確保更好的性能和一致性。

通過掌握這些概念和技術,你將能夠在 Nuxt3 中創建出富有表現力和高度互動的路由系統,為用戶提供流暢的導航體驗。


上一篇
Day 28: 使用 Pinia 與 Nuxt3 管理全局狀態
下一篇
Day 30: 最終結語:前端工程師素養與思維,台灣前端軟體工程師的困境和未來
系列文
Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言